/*
 * Copyright (c) 2019, Oracle and/or its affiliates. All rights reserved.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *  http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 *
 */

package org.eclipse.imagen;

import java.awt.Point;
import java.awt.Rectangle;
import java.awt.RenderingHints;
import java.awt.Shape;
import java.awt.geom.AffineTransform;
import java.awt.geom.Point2D;
import java.awt.geom.Rectangle2D;
import java.awt.image.BufferedImage;
import java.awt.image.DataBuffer;
import java.awt.image.MultiPixelPackedSampleModel;
import java.awt.image.RenderedImage;
import java.awt.image.SampleModel;
import java.awt.image.renderable.ParameterBlock;
import java.awt.image.renderable.RenderedImageFactory;
import java.io.Serializable;
import java.util.Arrays;
import java.util.LinkedList;
import java.util.ListIterator;
import java.util.Vector;
import org.eclipse.imagen.iterator.RandomIter;
import org.eclipse.imagen.iterator.RandomIterFactory;
import org.eclipse.imagen.media.util.ImageUtil;

/**
 * The parent class for representations of a region of interest of an image (currently only single band images with
 * integral data types are supported). This class represents region information in image form, and can thus be used as a
 * fallback where a <code>Shape</code> representation is unavailable. Where possible, subclasses such as ROIShape are
 * used since they provide a more compact means of storage for large regions.
 *
 * <p>The getAsShape() method may be called optimistically on any instance of ROI; however, it may return null to
 * indicate that a <code>Shape</code> representation of the ROI is not available. In this case, getAsImage() should be
 * called as a fallback.
 *
 * <p>Inclusion and exclusion of pixels is defined by a threshold value. Pixel values greater than or equal to the
 * threshold indicate inclusion.
 */
public class ROI implements Serializable {

    /** A RandomIter used to grab pixels from the ROI. */
    private transient RandomIter iter = null;

    /** The <code>PlanarImage</code> representation of the ROI. */
    transient PlanarImage theImage = null;

    /** The inclusion/exclusion threshold of the ROI. */
    int threshold = 127;

    /**
     * Merge a <code>LinkedList</code> of <code>Rectangle</code>s representing run lengths of pixels in the ROI into a
     * minimal list wherein vertically abutting <code>Rectangle</code>s are merged. The operation is effected in place.
     *
     * @param rectList The list of run length <code>Rectangle</code>s.
     * @throws IllegalArgumentException if rectList is null.
     * @return The merged list.
     */
    protected static LinkedList mergeRunLengthList(LinkedList rectList) {

        if (rectList == null) {
            throw new IllegalArgumentException(JaiI18N.getString("Generic0"));
        }

        // Merge the run length rectangles if more than one was detected.
        if (rectList.size() > 1) {
            // Traverse the list sequentially merging all subsequent
            // vertically abutting Rectangles with the same abscissa
            // origin and width with the current starting Rectangle.
            for (int mergeIndex = 0; mergeIndex < rectList.size() - 1; mergeIndex++) {

                ListIterator rectIter = rectList.listIterator(mergeIndex);
                Rectangle mergeRect = (Rectangle) rectIter.next();

                while (rectIter.hasNext()) {
                    Rectangle runRect = (Rectangle) rectIter.next();

                    // Calculate ordinate value of abutting rectangle.
                    int abuttingY = mergeRect.y + mergeRect.height;

                    if (runRect.y == abuttingY && runRect.x == mergeRect.x && runRect.width == mergeRect.width) {
                        mergeRect = new Rectangle(
                                mergeRect.x, mergeRect.y, mergeRect.width, mergeRect.height + runRect.height);

                        // Remove "runRect" from the list.
                        rectIter.remove();

                        // Replace "mergeRect" with updated version.
                        rectList.set(mergeIndex, (Object) mergeRect);
                    } else if (runRect.y > abuttingY) {
                        // All Rectangles in the list with index greater than
                        // mergeIndex are runlength Rectangles and are sorted
                        // in non-decreasing ordinate order. Therefore there
                        // are no more Rectangles which could possibly be
                        // merged with mergeRect.
                        break;
                    }
                }
            }
        }

        return rectList;
    }

    /**
     * The default constructor.
     *
     * <p>Using this constructor means that the subclass must override all methods that reference theImage.
     */
    protected ROI() {}

    /**
     * Constructs an ROI from a RenderedImage. The inclusion threshold is taken to be halfway between the minimum and
     * maximum sample values specified by the image's SampleModel.
     *
     * @param im A single-banded RenderedImage.
     * @throws IllegalArgumentException if im is null.
     * @throws IllegalArgumentException if im does not have exactly one band
     */
    public ROI(RenderedImage im) {
        this(im, 127);
    }

    /**
     * Constructs an ROI from a RenderedImage. The inclusion threshold is specified explicitly.
     *
     * @param im A single-banded RenderedImage.
     * @param threshold The desired inclusion threshold.
     * @throws IllegalArgumentException if im is null.
     * @throws IllegalArgumentException if im does not have exactly one band
     */
    public ROI(RenderedImage im, int threshold) {

        if (im == null) {
            throw new IllegalArgumentException(JaiI18N.getString("Generic0"));
        }

        SampleModel sm = im.getSampleModel();

        if (sm.getNumBands() != 1) {
            throw new IllegalArgumentException(JaiI18N.getString("ROI0"));
        }

        this.threshold = threshold;

        // If the image is already binary and the threshold is >1
        // then there is no work to do.
        if ((threshold >= 1) && ImageUtil.isBinary(sm)) {
            theImage = PlanarImage.wrapRenderedImage(im);

            // Otherwise binarize the image for efficiency.
        } else {

            ParameterBlockJAI pbj = new ParameterBlockJAI("binarize");

            pbj.setSource("source0", im);
            pbj.setParameter("threshold", (double) threshold);

            theImage = JAI.create("binarize", pbj, null);
        }
    }

    /** Get the iterator, construct it if need be. */
    private RandomIter getIter() {
        if (iter == null) {
            iter = RandomIterFactory.create(theImage, null);
        }
        return iter;
    }

    /** Returns the inclusion/exclusion threshold value. */
    public int getThreshold() {
        return threshold;
    }

    /** Sets the inclusion/exclusion threshold value. */
    public void setThreshold(int threshold) {
        this.threshold = threshold;
        ((RenderedOp) theImage).setParameter((double) threshold, 0);
        iter = null;
        getIter();
    }

    /** Returns the bounds of the ROI as a <code>Rectangle</code>. */
    public Rectangle getBounds() {
        return new Rectangle(theImage.getMinX(), theImage.getMinY(), theImage.getWidth(), theImage.getHeight());
    }

    /** Returns the bounds of the ROI as a <code>Rectangle2D</code>. */
    public Rectangle2D getBounds2D() {
        return new Rectangle2D.Float(
                (float) theImage.getMinX(), (float) theImage.getMinY(), (float) theImage.getWidth(), (float)
                        theImage.getHeight());
    }

    /**
     * Returns <code>true</code> if the ROI contains a given Point.
     *
     * @param p A Point identifying the pixel to be queried.
     * @throws IllegalArgumentException if p is null.
     * @return <code>true</code> if the pixel lies within the ROI.
     */
    public boolean contains(Point p) {
        if (p == null) {
            throw new IllegalArgumentException(JaiI18N.getString("Generic0"));
        }

        return contains(p.x, p.y);
    }

    /**
     * Returns <code>true</code> if the ROI contains a given Point2D.
     *
     * @param p A Point2D identifying the pixel to be queried.
     * @throws IllegalArgumentException if p is null.
     * @return <code>true</code> if the pixel lies within the ROI.
     */
    public boolean contains(Point2D p) {
        if (p == null) {
            throw new IllegalArgumentException(JaiI18N.getString("Generic0"));
        }

        return contains((int) p.getX(), (int) p.getY());
    }

    /**
     * Returns <code>true</code> if the ROI contains the point (x, y).
     *
     * @param x An int specifying the X coordinate of the pixel to be queried.
     * @param y An int specifying the Y coordinate of the pixel to be queried.
     * @return <code>true</code> if the pixel lies within the ROI.
     */
    public boolean contains(int x, int y) {
        int minX = theImage.getMinX();
        int minY = theImage.getMinY();

        return (x >= minX && x < minX + theImage.getWidth())
                && (y >= minY && y < minY + theImage.getHeight())
                && (getIter().getSample(x, y, 0) >= 1);
    }

    /**
     * Returns <code>true</code> if the ROI contain the point (x, y).
     *
     * @param x A double specifying the X coordinate of the pixel to be queried.
     * @param y A double specifying the Y coordinate of the pixel to be queried.
     * @return <code>true</code> if the pixel lies within the ROI.
     */
    public boolean contains(double x, double y) {
        return contains((int) x, (int) y);
    }

    /**
     * Returns <code>true</code> if a given <code>Rectangle</code> is entirely included within the ROI.
     *
     * @param rect A <code>Rectangle</code> specifying the region to be tested for inclusion.
     * @throws IllegalArgumentException if rect is null.
     * @return <code>true</code> if the rectangle is entirely contained within the ROI.
     */
    public boolean contains(Rectangle rect) {
        if (rect == null) {
            throw new IllegalArgumentException(JaiI18N.getString("Generic0"));
        }

        if (!rect.equals(rect.intersection(getBounds()))) {
            return false;
        }

        byte[] packedData = ImageUtil.getPackedBinaryData(theImage.getData(), rect);

        // ImageUtil.getPackedBinaryData does not zero out the extra
        // bits used to pad to the nearest byte - therefore ignore
        // these bits.
        int leftover = rect.width % 8;

        if (leftover == 0) {

            for (int i = 0; i < packedData.length; i++) if ((packedData[i] & 0xff) != 0xff) return false;

        } else {

            int mask = ((1 << leftover) - 1) << (8 - leftover);

            for (int y = 0, k = 0; y < rect.height; y++) {
                for (int x = 0; x < rect.width - leftover; x += 8, k++) {
                    if ((packedData[k] & 0xff) != 0xff) return false;
                }

                if ((packedData[k] & mask) != mask) return false;

                k++;
            }
        }

        return true;
    }

    /**
     * Returns <code>true</code> if a given <code>Rectangle2D</code> is entirely included within the ROI.
     *
     * @param rect A <code>Rectangle2D</code> specifying the region to be tested for inclusion.
     * @throws IllegalArgumentException if rect is null.
     * @return <code>true</code> if the rectangle is entirely contained within the ROI.
     */
    public boolean contains(Rectangle2D rect) {
        if (rect == null) {
            throw new IllegalArgumentException(JaiI18N.getString("Generic0"));
        }
        Rectangle r =
                new Rectangle((int) rect.getX(), (int) rect.getY(), (int) rect.getWidth(), (int) rect.getHeight());
        return contains(r);
    }

    /**
     * Returns <code>true</code> if a given rectangle (x, y, w, h) is entirely included within the ROI.
     *
     * @param x The int X coordinate of the upper left corner of the region.
     * @param y The int Y coordinate of the upper left corner of the region.
     * @param w The int width of the region.
     * @param h The int height of the region.
     * @return <code>true</code> if the rectangle is entirely contained within the ROI.
     */
    public boolean contains(int x, int y, int w, int h) {
        Rectangle r = new Rectangle(x, y, w, h);
        return contains(r);
    }

    /**
     * Returns <code>true</code> if a given rectangle (x, y, w, h) is entirely included within the ROI.
     *
     * @param x The double X coordinate of the upper left corner of the region.
     * @param y The double Y coordinate of the upper left corner of the region.
     * @param w The double width of the region.
     * @param h The double height of the region.
     * @return <code>true</code> if the rectangle is entirely contained within the ROI.
     */
    public boolean contains(double x, double y, double w, double h) {
        Rectangle rect = new Rectangle(
                (int) x, (int) y,
                (int) w, (int) h);
        return contains(rect);
    }

    /**
     * Returns <code>true</code> if a given <code>Rectangle</code> intersects the ROI.
     *
     * @param rect A <code>Rectangle</code> specifying the region to be tested for inclusion.
     * @throws IllegalArgumentException if rect is null.
     * @return <code>true</code> if the rectangle intersects the ROI.
     */
    public boolean intersects(Rectangle rect) {
        if (rect == null) {
            throw new IllegalArgumentException(JaiI18N.getString("Generic0"));
        }

        Rectangle r = rect.intersection(getBounds());

        if (r.isEmpty()) {
            return false;
        }

        byte[] packedData = ImageUtil.getPackedBinaryData(theImage.getData(), r);

        // ImageUtil.getPackedBinaryData does not zero out the extra
        // bits used to pad to the nearest byte - therefore ignore
        // these bits.
        int leftover = r.width % 8;

        if (leftover == 0) {

            for (int i = 0; i < packedData.length; i++) if ((packedData[i] & 0xff) != 0) return true;

        } else {

            int mask = ((1 << leftover) - 1) << (8 - leftover);

            for (int y = 0, k = 0; y < r.height; y++) {
                for (int x = 0; x < r.width - leftover; x += 8, k++) {
                    if ((packedData[k] & 0xff) != 0) return true;
                }
                if ((packedData[k] & mask) != 0) return true;
                k++;
            }
        }

        return false;
    }

    /**
     * Returns <code>true</code> if a given <code>Rectangle2D</code> intersects the ROI.
     *
     * @param r A <code>Rectangle2D</code> specifying the region to be tested for inclusion.
     * @throws IllegalArgumentException if r is null.
     * @return <code>true</code> if the rectangle intersects the ROI.
     */
    public boolean intersects(Rectangle2D r) {
        if (r == null) {
            throw new IllegalArgumentException(JaiI18N.getString("Generic0"));
        }

        Rectangle rect = new Rectangle((int) r.getX(), (int) r.getY(), (int) r.getWidth(), (int) r.getHeight());
        return intersects(rect);
    }

    /**
     * Returns <code>true</code> if a given rectangular region intersects the ROI.
     *
     * @param x The int X coordinate of the upper left corner of the region.
     * @param y The int Y coordinate of the upper left corner of the region.
     * @param w The int width of the region.
     * @param h The int height of the region.
     * @return <code>true</code> if the rectangle intersects the ROI.
     */
    public boolean intersects(int x, int y, int w, int h) {
        Rectangle rect = new Rectangle(x, y, w, h);
        return intersects(rect);
    }

    /**
     * Returns <code>true</code> if a given rectangular region intersects the ROI.
     *
     * @param x The double X coordinate of the upper left corner of the region.
     * @param y The double Y coordinate of the upper left corner of the region.
     * @param w The double width of the region.
     * @param h The double height of the region.
     * @return <code>true</code> if the rectangle intersects the ROI.
     */
    public boolean intersects(double x, double y, double w, double h) {
        Rectangle rect = new Rectangle(
                (int) x, (int) y,
                (int) w, (int) h);
        return intersects(rect);
    }

    /** Create a binary PlanarImage of the size/bounds specified by the rectangle. */
    private static PlanarImage createBinaryImage(Rectangle r) {

        if ((r.x == 0) && (r.y == 0)) {

            BufferedImage bi = new BufferedImage(r.width, r.height, BufferedImage.TYPE_BYTE_BINARY);

            return PlanarImage.wrapRenderedImage(bi);

        } else {

            SampleModel sm = new MultiPixelPackedSampleModel(DataBuffer.TYPE_BYTE, r.width, r.height, 1);

            // Create a TiledImage into which to write.
            return new TiledImage(r.x, r.y, r.width, r.height, r.x, r.y, sm, PlanarImage.createColorModel(sm));
        }
    }

    /**
     * Creates a merged ROI by performing the specified image operation on <code>this</code> image and the image of the
     * specified ROI.
     *
     * @param ROI the ROI to merge with <code>this</code>
     * @param op the JAI operator to use for merge.
     * @return the merged ROI
     */
    private ROI createOpROI(ROI roi, String op) {

        if (roi == null) {
            throw new IllegalArgumentException(JaiI18N.getString("Generic0"));
        }

        PlanarImage imThis = this.getAsImage();
        PlanarImage imROI = roi.getAsImage();
        PlanarImage imDest;

        Rectangle boundsThis = imThis.getBounds();
        Rectangle boundsROI = imROI.getBounds();

        // If the bounds of the two images do not match, then
        // expand as necessary to the union of the two bounds
        // using the "overlay" operator and then perform the JAI
        // operation.
        if (op.equals("and") || boundsThis.equals(boundsROI)) {
            imDest = JAI.create(op, imThis, imROI);

        } else if (op.equals("subtract") || boundsThis.contains(boundsROI)) {

            PlanarImage imBounds = createBinaryImage(boundsThis);

            imBounds = JAI.create("overlay", imBounds, imROI);
            imDest = JAI.create(op, imThis, imBounds);

        } else if (boundsROI.contains(boundsThis)) {

            PlanarImage imBounds = createBinaryImage(boundsROI);

            imBounds = JAI.create("overlay", imBounds, imThis);
            imDest = JAI.create(op, imBounds, imROI);

        } else {

            Rectangle merged = boundsThis.union(boundsROI);

            PlanarImage imBoundsThis = createBinaryImage(merged);
            PlanarImage imBoundsROI = createBinaryImage(merged);

            imBoundsThis = JAI.create("overlay", imBoundsThis, imThis);
            imBoundsROI = JAI.create("overlay", imBoundsROI, imROI);
            imDest = JAI.create(op, imBoundsThis, imBoundsROI);
        }

        return new ROI(imDest, threshold);
    }

    /**
     * Adds another <code>ROI</code> to this one and returns the result as a new <code>ROI</code>. The supplied <code>
     * ROI</code> will be converted to a rendered form if necessary. The bounds of the resultant <code>ROI</code> will
     * be the union of the bounds of the two <code>ROI</code>s being merged.
     *
     * @param roi An ROI.
     * @throws IllegalArgumentException if roi is null.
     * @return A new ROI containing the new ROI data.
     */
    public ROI add(ROI roi) {
        return createOpROI(roi, "add");
    }

    /**
     * Subtracts another <code>ROI</code> from this one and returns the result as a new <code>ROI</code>. The supplied
     * <code>ROI</code> will be converted to a rendered form if necessary. The bounds of the resultant <code>ROI</code>
     * will be the same as <code>this</code> <code>ROI</code>.
     *
     * @param roi An ROI.
     * @throws IllegalArgumentException if roi is null.
     * @return A new ROI containing the new ROI data.
     */
    public ROI subtract(ROI roi) {
        return createOpROI(roi, "subtract");
    }

    /**
     * Intersects the <code>ROI</code> with another <code>ROI</code> and returns the result as a new <code>ROI</code>.
     * The supplied <code>ROI</code> will be converted to a rendered form if necessary. The bounds of the resultant
     * <code>ROI</code> will be the intersection of the bounds of the two <code>ROI</code>s being merged.
     *
     * @param roi An ROI.
     * @throws IllegalArgumentException if roi is null.
     * @return A new ROI containing the new ROI data.
     */
    public ROI intersect(ROI roi) {
        return createOpROI(roi, "and");
    }

    /**
     * Exclusive-ors the <code>ROI</code> with another <code>ROI</code> and returns the result as a new <code>ROI</code>
     * . The supplied <code>ROI</code> will be converted to a rendered form if necessary. The bounds of the resultant
     * <code>ROI</code> will be the union of the bounds of the two <code>ROI</code>s being merged.
     *
     * @param roi An ROI.
     * @throws IllegalArgumentException if roi is null.
     * @return A new ROI containing the new ROI data.
     */
    public ROI exclusiveOr(ROI roi) {
        return createOpROI(roi, "xor");
    }

    /**
     * Performs an affine transformation and returns the result as a new ROI. The transformation is performed by an
     * "Affine" RIF using the indicated interpolation method.
     *
     * @param at an AffineTransform specifying the transformation.
     * @param interp the Interpolation to be used.
     * @throws IllegalArgumentException if at is null.
     * @throws IllegalArgumentException if interp is null.
     * @return a new ROI containing the transformed ROI data.
     */
    public ROI transform(AffineTransform at, Interpolation interp) {

        if (at == null) {
            throw new IllegalArgumentException(JaiI18N.getString("ROI5"));
        }

        if (interp == null) {
            throw new IllegalArgumentException(JaiI18N.getString("ROI6"));
        }

        ParameterBlock paramBlock = new ParameterBlock();
        paramBlock.add(at);
        paramBlock.add(interp);
        return performImageOp("Affine", paramBlock, 0, null);
    }

    /**
     * Performs an affine transformation and returns the result as a new ROI. The transformation is performed by an
     * "Affine" RIF using nearest neighbor interpolation.
     *
     * @param at an AffineTransform specifying the transformation.
     * @throws IllegalArgumentException if at is null.
     * @return a new ROI containing the transformed ROI data.
     */
    public ROI transform(AffineTransform at) {
        if (at == null) {
            throw new IllegalArgumentException(JaiI18N.getString("Generic0"));
        }

        return transform(at, Interpolation.getInstance(Interpolation.INTERP_NEAREST));
    }

    /**
     * Transforms an ROI using an imaging operation. The operation is specified by a <code>RenderedImageFactory</code>.
     * The operation's <code>ParameterBlock</code>, minus the image source itself is supplied, along with an index
     * indicating where to insert the ROI image. The <code>renderHints</code> argument allows rendering hints to be
     * passed in.
     *
     * @param RIF A <code>RenderedImageFactory</code> that will be used to create the op.
     * @param paramBlock A <code>ParameterBlock</code> containing all sources and parameters for the op except for the
     *     ROI itself.
     * @param sourceIndex The index of the <code>ParameterBlock</code>'s sources where the ROI is to be inserted.
     * @param renderHints A <code>RenderingHints</code> object containing rendering hints, or null.
     * @throws IllegalArgumentException if RIF is null.
     * @throws IllegalArgumentException if paramBlock is null.
     */
    public ROI performImageOp(
            RenderedImageFactory RIF, ParameterBlock paramBlock, int sourceIndex, RenderingHints renderHints) {

        if (RIF == null || paramBlock == null) {
            throw new IllegalArgumentException(JaiI18N.getString("Generic0"));
        }

        // Clone the ParameterBlock and insert a source
        ParameterBlock pb = (ParameterBlock) paramBlock.clone();
        Vector sources = pb.getSources();
        sources.insertElementAt(this.getAsImage(), sourceIndex);

        // Create a new RenderedImage based on the RIF
        // and ParameterBlock.
        RenderedImage im = RIF.create(pb, renderHints);
        return new ROI(im, threshold);
    }

    /**
     * Transforms an ROI using an imaging operation. The operation is specified by name; the default JAI registry is
     * used to resolve this into a RIF. The operation's <code>ParameterBlock</code>, minus the image source itself is
     * supplied, along with an index indicating where to insert the ROI image. The <code>renderHints</code> argument
     * allows rendering hints to be passed in.
     *
     * @param name The name of the operation to perform.
     * @param paramBlock A <code>ParameterBlock</code> containing all sources and parameters for the op except for the
     *     ROI itself.
     * @param sourceIndex The index of the <code>ParameterBlock</code>'s sources where the ROI is to be inserted.
     * @param renderHints A <code>RenderingHints</code> object containing rendering hints, or null.
     * @throws IllegalArgumentException if name is null.
     * @throws IllegalArgumentException if paramBlock is null.
     */
    public ROI performImageOp(String name, ParameterBlock paramBlock, int sourceIndex, RenderingHints renderHints) {

        if (name == null || paramBlock == null) {
            throw new IllegalArgumentException(JaiI18N.getString("Generic0"));
        }

        // Clone the ParameterBlock and insert a source
        ParameterBlock pb = (ParameterBlock) paramBlock.clone();
        Vector sources = pb.getSources();
        sources.insertElementAt(this.getAsImage(), sourceIndex);

        // Create a new RenderedImage based on the operation name
        // and ParameterBlock using the default registry.
        RenderedImage im = JAI.create(name, pb, renderHints);
        return new ROI(im, threshold);
    }

    /**
     * Returns a <code>Shape</code> representation of the <code>ROI</code>, if possible. If none is available, null is
     * returned. A proper instance of <code>ROI</code> (one that is not an instance of any subclass of <code>ROI</code>)
     * will always return null.
     *
     * @return The <code>ROI</code> as a <code>Shape</code>.
     */
    public Shape getAsShape() {
        return null;
    }

    /**
     * Returns a <code>PlanarImage</code> representation of the <code>ROI</code>. This method will always succeed. This
     * method returns a (bilevel) image whose <code>SampleModel</code> is an instance of <code>
     * MultiPixelPackedSampleModel</code>.
     *
     * @return The <code>ROI</code> as a <code>PlanarImage</code>.
     */
    public PlanarImage getAsImage() {
        return theImage;
    }

    /**
     * Returns a bitmask for a given rectangular region of the ROI indicating whether the pixel is included in the
     * region of interest. The results are packed into 32-bit integers, with the MSB considered to lie on the left. The
     * last entry in each row of the result may have bits that lie outside of the requested rectangle. These bits are
     * guaranteed to be zeroed.
     *
     * <p>The <code>mask</code> array, if supplied, must be of length equal to or greater than <code>height</code> and
     * each of its subarrays must have length equal to or greater than (width + 31)/32. If <code>null</code> is passed
     * in, a suitable array will be constructed. If the mask is non-null but has insufficient size, an exception will be
     * thrown.
     *
     * @param x The X coordinate of the upper left corner of the rectangle.
     * @param y The Y coordinate of the upper left corner of the rectangle.
     * @param width The width of the rectangle.
     * @param height The height of the rectangle.
     * @param mask A two-dimensional array of ints at least (width + 31)/32 entries wide and (height) entries tall, or
     *     null.
     * @return A reference to the <code>mask</code> parameter, or to a newly constructed array if <code>mask</code> is
     *     <code>null</code>. If the specified rectangle does intersect with the image bounds then a <code>null</code>
     *     is returned.
     */
    public int[][] getAsBitmask(int x, int y, int width, int height, int[][] mask) {

        Rectangle rect = getBounds().intersection(new Rectangle(x, y, width, height));

        // Verify that the requested area actually intersects the ROI image.
        if (rect.isEmpty()) {
            return null;
        }

        // Determine the minimum required width of the bitmask in integers.
        int bitmaskIntWidth = (width + 31) / 32;

        // Construct bitmask array if argument is null.
        if (mask == null) {
            mask = new int[height][bitmaskIntWidth];
        } else if (mask.length < height || mask[0].length < bitmaskIntWidth) {
            throw new RuntimeException(JaiI18N.getString("ROI3"));
        }

        byte[] data = ImageUtil.getPackedBinaryData(theImage.getData(), rect);

        // ImageUtil.getPackedBinaryData does not zero out the extra
        // bits used to pad to the nearest byte - so zero these
        // bits out.
        int leftover = rect.width % 8;

        if (leftover != 0) {
            int datamask = ((1 << leftover) - 1) << (8 - leftover);
            int linestride = (width + 7) / 8;

            for (int i = linestride - 1; i < data.length; i += linestride) {
                data[i] = (byte) (data[i] & datamask);
            }
        }

        int lineStride = (rect.width + 7) / 8;
        int leftOver = lineStride % 4;

        int row, col, k;
        int ncols = (lineStride - leftOver) / 4;

        for (row = 0, k = 0; row < rect.height; row++) {
            int[] maskRow = mask[row];

            for (col = 0; col < ncols; col++) {
                maskRow[col] = ((data[k] & 0xff) << 24)
                        | ((data[k + 1] & 0xff) << 16)
                        | ((data[k + 2] & 0xff) << 8)
                        | ((data[k + 3] & 0xff) << 0);
                k += 4;
            }

            switch (leftOver) {
                case 0:
                    break;
                case 1:
                    maskRow[col++] = ((data[k] & 0xff) << 24);
                    break;
                case 2:
                    maskRow[col++] = ((data[k] & 0xff) << 24) | ((data[k + 1] & 0xff) << 16);
                    break;
                case 3:
                    maskRow[col++] =
                            ((data[k] & 0xff) << 24) | ((data[k + 1] & 0xff) << 16) | ((data[k + 2] & 0xff) << 8);
                    break;
            }

            k += leftOver;

            Arrays.fill(maskRow, col, bitmaskIntWidth, 0);
        }

        // Clear any trailing rows.
        for (row = rect.height; row < height; row++) {
            Arrays.fill(mask[row], 0);
        }

        return mask;
    }

    /**
     * Returns a <code>LinkedList</code> of <code>Rectangle</code>s for a given rectangular region of the ROI. The
     * <code>Rectangle</code>s in the list are merged into a minimal set.
     *
     * @param x The X coordinate of the upper left corner of the rectangle.
     * @param y The Y coordinate of the upper left corner of the rectangle.
     * @param width The width of the rectangle.
     * @param height The height of the rectangle.
     * @return A <code>LinkedList</code> of <code>Rectangle</code>s. If the specified rectangle does intersect with the
     *     image bounds then a <code>null</code> is returned.
     */
    public LinkedList getAsRectangleList(int x, int y, int width, int height) {
        return getAsRectangleList(x, y, width, height, true);
    }

    /**
     * Returns a <code>LinkedList</code> of <code>Rectangle</code>s for a given rectangular region of the ROI.
     *
     * @param x The X coordinate of the upper left corner of the rectangle.
     * @param y The Y coordinate of the upper left corner of the rectangle.
     * @param width The width of the rectangle.
     * @param height The height of the rectangle.
     * @param mergeRectangles <code>true</code> if the <code>Rectangle</code>s are to be merged into a minimal set.
     * @return A <code>LinkedList</code> of <code>Rectangle</code>s. If the specified rectangle does intersect with the
     *     image bounds then a <code>null</code> is returned.
     */
    protected LinkedList getAsRectangleList(int x, int y, int width, int height, boolean mergeRectangles) {

        // Verify that the requested area actually intersects the ROI image.
        Rectangle bounds = getBounds();
        Rectangle rect = new Rectangle(x, y, width, height);
        if (!bounds.intersects(rect)) {
            return null;
        }

        // Clip the requested area to the ROI image if necessary.
        if (!bounds.contains(rect)) {
            rect = bounds.intersection(rect);
            x = rect.x;
            y = rect.y;
            width = rect.width;
            height = rect.height;
        }

        byte[] data = ImageUtil.getPackedBinaryData(theImage.getData(), rect);

        // ImageUtil.getPackedBinaryData does not zero out the extra
        // bits used to pad to the nearest byte - therefore ignore
        // these bits.
        int lineStride = (width + 7) / 8;
        int leftover = width % 8;
        int mask = (leftover == 0) ? 0xff : ((1 << leftover) - 1) << (8 - leftover);

        LinkedList rectList = new LinkedList();

        // Calculate the initial list of rectangles as a list of run
        // lengths which are in fact rectangles of unit height.

        int row, col, k, start, val, cnt;

        for (row = 0, k = 0; row < height; row++) {

            start = -1;

            for (col = 0, cnt = 0; col < lineStride; col++, k++) {

                val = data[k] & ((col == lineStride - 1) ? mask : 0xff);

                if (val == 0) {
                    if (start >= 0) {
                        rectList.addLast(new Rectangle(x + start, y + row, col * 8 - start, 1));
                        start = -1;
                    }

                } else if (val == 0xff) {
                    if (start < 0) {
                        start = col * 8;
                    }

                } else {
                    for (int bit = 7; bit >= 0; bit--) {
                        if ((val & (1 << bit)) == 0x00) {
                            if (start >= 0) {
                                rectList.addLast(new Rectangle(x + start, y + row, col * 8 + (7 - bit) - start, 1));
                                start = -1;
                            }
                        } else {
                            if (start < 0) {
                                start = col * 8 + (7 - bit);
                            }
                        }
                    }
                }
            }

            if (start >= 0) {
                rectList.addLast(new Rectangle(x + start, y + row, col * 8 - start, 1));
            }
        }

        // Return the list of Rectangles possibly merged into a minimal set.
        return mergeRectangles ? mergeRunLengthList(rectList) : rectList;
    }

    /**
     * Serialize the <code>ROI</code>.
     *
     * @param out The <code>ObjectOutputStream</code>.
     */
    /* TODO check serialization
    private void writeObject(ObjectOutputStream out) throws IOException {
        out.defaultWriteObject();
        if (theImage != null) {
            out.writeBoolean(true);
            RenderingHints hints = new RenderingHints(null);
            hints.put(JAI.KEY_SERIALIZE_DEEP_COPY, new Boolean(true));
            out.writeObject(SerializerFactory.getState(theImage, hints));
        } else {
            out.writeBoolean(false);
        }
    }*/

    /**
     * Deserialize the <code>ROI</code>.
     *
     * @param in The <code>ObjectInputStream</code>.
     */
    /* TODO check serialization
    private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException {
        in.defaultReadObject();
        if ((boolean) in.readBoolean()) {
            SerializableState ss = (SerializableState) in.readObject();
            RenderedImage ri = (RenderedImage) (ss.getObject());
            theImage = PlanarImage.wrapRenderedImage(ri);
        } else {
            theImage = null;
        }
        iter = null;
    }*/
}
